/*ESP21 Hourglass on 16x16 Matrix WS2812b by mircemk, April 2025 */ #include #define LED_PIN 5 #define NUM_LEDS 256 #define BRIGHTNESS 64 #define LED_TYPE WS2812B #define COLOR_ORDER GRB #define TILT_PIN 4 // D4 pin for tilt switch #define BUZZER_PIN 2 // Choose an available digital pin for the buzzer CRGB leds[NUM_LEDS]; // Colors CRGB BLACK = CRGB(0, 0, 0); CRGB MAGENTA = CRGB(255, 0, 255); CRGB YELLOW = CRGB(255, 255, 0); CRGB WHITE = CRGB(255, 255, 255); // Color for digits CRGB PALE_PURPLE = CRGB(0, 0, 0); // Very dim purple for outside dots CRGB PALE_RED = CRGB(7,15, 15); // Very dim red for inside dots // Animation timing const unsigned long PARTICLE_FALL_TIME = 2000; // 2 seconds per particle const int TOTAL_PARTICLES = 30; const unsigned long RESTART_DELAY = 60000; // 1 minute // Grid dimensions const int GRID_WIDTH = 16; const int GRID_HEIGHT = 16; // Digit display positions (7th row from top, 3 pixels from edges) const int LEFT_DIGIT_X = 0; // Changed from 3 to 0 (far left) const int RIGHT_DIGIT_X = 13; // Changed from 10 to 13 (far right) const int DIGIT_Y = 6; // Keep the same vertical position const int START_TONES[] = {300, 600, 900}; // Starting sequence frequencies const int TICK_TONE = 100; // Countdown tick frequency const int END_TONES[] = {900, 600, 300}; // Ending sequence frequencies const int START_END_TONE_DURATION = 200; // Duration for start/end tones in ms const int TICK_TONE_DURATION = 50; // Duration for tick tone in ms bool displayRotated = false; // Track if display is rotated unsigned long lastTiltCheck = 0; // Debouncing const unsigned long TILT_CHECK_DELAY = 50; // Check tilt every 50ms unsigned long lastSecond = 60; // Track last second for tone bool startTonesPlayed = false; // Track if start tones have been played bool endTonesPlayed = false; // Track if end tones have been played // Tracking variables unsigned long startTime = 0; unsigned long currentTime = 0; int particlesFallen = 0; bool animationComplete = false; // Use a 1D array to track sand (1=sand, 0=no sand) byte sandState[NUM_LEDS]; // Falling particle bool fallingParticle = false; uint8_t fallingParticleX = 0; uint8_t fallingParticleY = 0; unsigned long fallingStartTime = 0; // Convert x,y coordinates to LED index (assuming serpentine layout) uint16_t XY(uint8_t x, uint8_t y) { uint16_t i; if (displayRotated) { // If rotated, flip both x and y coordinates x = GRID_WIDTH - 1 - x; y = GRID_HEIGHT - 1 - y; } if(y & 0x01) { // Odd rows run backwards uint8_t reverseX = (GRID_WIDTH-1) - x; i = (y * GRID_WIDTH) + reverseX; } else { // Even rows run forwards i = (y * GRID_WIDTH) + x; } return i; } // 5x3 Font Data for digits 0-9 (full 5x3 matrix design) const byte DIGITS[10][5][3] = { { // 0 {1,1,1}, {1,0,1}, {1,0,1}, {1,0,1}, {1,1,1} }, { // 1 {0,1,0}, {1,1,0}, {0,1,0}, {0,1,0}, {1,1,1} }, { // 2 {1,1,1}, {0,0,1}, {1,1,1}, {1,0,0}, {1,1,1} }, { // 3 {1,1,1}, {0,0,1}, {1,1,1}, {0,0,1}, {1,1,1} }, { // 4 {1,0,1}, {1,0,1}, {1,1,1}, {0,0,1}, {0,0,1} }, { // 5 {1,1,1}, {1,0,0}, {1,1,1}, {0,0,1}, {1,1,1} }, { // 6 {1,1,1}, {1,0,0}, {1,1,1}, {1,0,1}, {1,1,1} }, { // 7 {1,1,1}, {0,0,1}, {0,1,0}, {1,0,0}, {1,0,0} }, { // 8 {1,1,1}, {1,0,1}, {1,1,1}, {1,0,1}, {1,1,1} }, { // 9 {1,1,1}, {1,0,1}, {1,1,1}, {0,0,1}, {1,1,1} } }; // drawDigit function to handle 5x3 digits void drawDigit(int digit, int xPos, int yPos, CRGB color) { for (int y = 0; y < 5; y++) { // Changed from 6 to 5 for (int x = 0; x < 3; x++) { if (DIGITS[digit][y][x]) { // Reverse the x-coordinate by drawing from right to left leds[XY(xPos + (2 - x), yPos + y)] = color; } } } } // Function to draw countdown number void drawCountdown(int seconds) { int tens = seconds / 10; int ones = seconds % 10; // Draw tens digit on the right drawDigit(tens, RIGHT_DIGIT_X, DIGIT_Y, WHITE); // Draw ones digit on the left drawDigit(ones, LEFT_DIGIT_X, DIGIT_Y, WHITE); } // Check if a position is within the hourglass container bool isInsideHourglass(uint8_t x, uint8_t y) { // Top half if (y <= 7) { if (y == 0 && x >= 1 && x <= 14) return true; if (y >= 1 && y <= 3 && x >= 2 && x <= 13) return true; if (y == 4 && x >= 3 && x <= 12) return true; if (y == 5 && x >= 4 && x <= 11) return true; if (y == 6 && x >= 5 && x <= 10) return true; if (y == 7 && x >= 6 && x <= 9) return true; } // Bottom half else { if (y == 8 && x >= 6 && x <= 9) return true; if (y == 9 && x >= 5 && x <= 10) return true; if (y == 10 && x >= 4 && x <= 11) return true; if (y == 11 && x >= 3 && x <= 12) return true; if (y >= 12 && y <= 14 && x >= 2 && x <= 13) return true; if (y == 15 && x >= 1 && x <= 14) return true; } return false; } // Check if a position is part of the hourglass outline bool isHourglassOutline(uint8_t x, uint8_t y) { // Top base (row 0) if (y == 0 && x >= 1 && x <= 14) return true; // Vertical walls - top half if (y >= 1 && y <= 3 && (x == 2 || x == 13)) return true; if (y == 4 && (x == 3 || x == 12)) return true; if (y == 5 && (x == 4 || x == 11)) return true; if (y == 6 && (x == 5 || x == 10)) return true; if (y == 7 && (x == 6 || x == 9)) return true; // Neck - only the sides, keeping the middle open if (y == 7 && (x == 7 || x == 8)) return false; if (y == 8 && (x == 7 || x == 8)) return false; // Vertical walls - bottom half if (y == 8 && (x == 6 || x == 9)) return true; if (y == 9 && (x == 5 || x == 10)) return true; if (y == 10 && (x == 4 || x == 11)) return true; if (y == 11 && (x == 3 || x == 12)) return true; if (y >= 12 && y <= 14 && (x == 2 || x == 13)) return true; // Bottom base (row 15) if (y == 15 && x >= 1 && x <= 14) return true; return false; } // Check if a position is in the neck area bool isNeckPosition(uint8_t x, uint8_t y) { return ((y == 7 || y == 8) && (x == 7 || x == 8)); } // Initialize the hourglass with sand particles // Initialize the hourglass with sand particles void initHourglass() { // First, set the background colors instead of clearing to black for (uint8_t y = 0; y < GRID_HEIGHT; y++) { for (uint8_t x = 0; x < GRID_WIDTH; x++) { if (isInsideHourglass(x, y) && !isHourglassOutline(x, y)) { // Inside hourglass - pale red background sandState[XY(x, y)] = 0; // Initialize as empty leds[XY(x, y)] = PALE_RED; } else if (!isInsideHourglass(x, y)) { // Outside hourglass - pale purple background sandState[XY(x, y)] = 0; leds[XY(x, y)] = PALE_PURPLE; } else { // Areas that will be outline sandState[XY(x, y)] = 0; leds[XY(x, y)] = BLACK; } } } int particleCount = 0; // First add 2 particles in the upper neck area (y=7) sandState[XY(7, 7)] = 1; // First neck particle sandState[XY(8, 7)] = 1; // Second neck particle particleCount = 2; // Fill the remaining 28 particles in the top half for (uint8_t y = 3; y <= 7; y++) { for (uint8_t x = 0; x < GRID_WIDTH && particleCount < TOTAL_PARTICLES; x++) { if (isInsideHourglass(x, y) && !isHourglassOutline(x, y) && !(x == 7 && y == 7) && !(x == 8 && y == 7) && // Skip the neck positions we already filled sandState[XY(x, y)] == 0) { sandState[XY(x, y)] = 1; // Add sand particleCount++; } } } // Draw initial state for (uint8_t y = 0; y < GRID_HEIGHT; y++) { for (uint8_t x = 0; x < GRID_WIDTH; x++) { if (sandState[XY(x, y)] == 1) { leds[XY(x, y)] = YELLOW; // Draw sand particles } else if (isHourglassOutline(x, y)) { leds[XY(x, y)] = MAGENTA; // Draw outline } } } FastLED.show(); // Show the initial state particlesFallen = 0; animationComplete = false; } // Find a sand particle in the top container to drop bool findSandParticleToRemove(uint8_t* outX, uint8_t* outY) { for (uint8_t y = 3; y <= 7; y++) { int particleXPositions[GRID_WIDTH]; int particleYPositions[GRID_WIDTH]; int count = 0; for (uint8_t x = 0; x < GRID_WIDTH; x++) { if (isInsideHourglass(x, y) && !isHourglassOutline(x, y) && sandState[XY(x, y)] == 1) { particleXPositions[count] = x; particleYPositions[count] = y; count++; } } if (count > 0) { int randomIndex = random(count); *outX = particleXPositions[randomIndex]; *outY = particleYPositions[randomIndex]; sandState[XY(*outX, *outY)] = 0; // Remove this particle return true; } } return false; } // Find a position in the bottom container bool findPositionInBottomContainer(uint8_t* outX, uint8_t* outY) { for (uint8_t y = 15; y >= 9; y--) { int availableSpots[GRID_WIDTH]; int count = 0; for (uint8_t x = 0; x < GRID_WIDTH; x++) { if (isInsideHourglass(x, y) && !isHourglassOutline(x, y) && sandState[XY(x, y)] == 0) { availableSpots[count] = x; count++; } } if (count > 0) { int randomIndex = random(count); *outX = availableSpots[randomIndex]; *outY = y; return true; } } return false; } // Start a new falling particle void startNewFallingParticle() { uint8_t startX, startY; if (!findSandParticleToRemove(&startX, &startY)) { fallingParticle = false; return; } fallingParticleX = startX; fallingParticleY = startY; fallingParticle = true; fallingStartTime = millis(); particlesFallen++; } // Update falling particle position void updateFallingParticle() { if (!fallingParticle) return; unsigned long elapsed = millis() - fallingStartTime; if (elapsed >= PARTICLE_FALL_TIME) { fallingParticle = false; uint8_t endX, endY; if (findPositionInBottomContainer(&endX, &endY)) { sandState[XY(endX, endY)] = 1; } if (particlesFallen < TOTAL_PARTICLES) { startNewFallingParticle(); } else { animationComplete = true; } return; } float progress = (float)elapsed / PARTICLE_FALL_TIME; uint8_t targetX = (fallingParticleX < 8) ? 7 : 8; if (progress < 0.5) { float neckProgress = progress * 2; fallingParticleX = fallingParticleX + (neckProgress * (targetX - fallingParticleX)); fallingParticleY = fallingParticleY + (neckProgress * (7 - fallingParticleY)); } else { float bottomProgress = (progress - 0.5) * 2; fallingParticleX = targetX; uint8_t endY; uint8_t endX; findPositionInBottomContainer(&endX, &endY); fallingParticleY = 8 + (bottomProgress * (endY - 8)); } } void playTone(int frequency, int duration) { tone(BUZZER_PIN, frequency, duration); } void playStartSequence() { for (int i = 0; i < 3; i++) { playTone(START_TONES[i], START_END_TONE_DURATION); delay(START_END_TONE_DURATION); } startTonesPlayed = true; } void playEndSequence() { for (int i = 0; i < 3; i++) { playTone(END_TONES[i], START_END_TONE_DURATION); delay(START_END_TONE_DURATION); } endTonesPlayed = true; } void setup() { delay(1000); pinMode(BUZZER_PIN, OUTPUT); pinMode(TILT_PIN, INPUT_PULLUP); FastLED.addLeds(leds, NUM_LEDS).setCorrection(TypicalLEDStrip); FastLED.setBrightness(BRIGHTNESS); // Initial orientation check displayRotated = !digitalRead(TILT_PIN); // Invert because of pull-up randomSeed(analogRead(0)); initHourglass(); startTime = millis(); startNewFallingParticle(); startTonesPlayed = false; // Reset start tones flag endTonesPlayed = false; // Reset end tones flag } void loop() { currentTime = millis(); // Check tilt switch with debouncing if (currentTime - lastTiltCheck >= TILT_CHECK_DELAY) { bool newRotation = !digitalRead(TILT_PIN); // Invert because of pull-up if (newRotation != displayRotated) { displayRotated = newRotation; // Reset hourglass when flipped initHourglass(); startTime = currentTime; particlesFallen = 0; animationComplete = false; startNewFallingParticle(); } lastTiltCheck = currentTime; } // Calculate remaining time int remainingSeconds = 60; if (currentTime > startTime) { unsigned long elapsedTime = currentTime - startTime; if (elapsedTime < RESTART_DELAY) { remainingSeconds = (RESTART_DELAY - elapsedTime + 999) / 1000; } else { remainingSeconds = 0; } } // Play start sequence if not yet played if (!startTonesPlayed && remainingSeconds == 60) { playStartSequence(); } // Play tick tone when second changes if (remainingSeconds < lastSecond && remainingSeconds > 0) { playTone(TICK_TONE, TICK_TONE_DURATION); } lastSecond = remainingSeconds; // Play end sequence when countdown reaches zero if (remainingSeconds == 0 && !endTonesPlayed && animationComplete) { playEndSequence(); } // If animation is complete and time is up, show final state if (animationComplete && (currentTime - startTime >= RESTART_DELAY)) { // Draw background colors first for (uint8_t y = 0; y < GRID_HEIGHT; y++) { for (uint8_t x = 0; x < GRID_WIDTH; x++) { if (isInsideHourglass(x, y) && !isHourglassOutline(x, y)) { leds[XY(x, y)] = PALE_RED; // Inside hourglass background } else if (!isInsideHourglass(x, y)) { leds[XY(x, y)] = PALE_PURPLE; // Outside hourglass background } } } // Draw final hourglass state for (uint8_t y = 0; y < GRID_HEIGHT; y++) { for (uint8_t x = 0; x < GRID_WIDTH; x++) { if (isHourglassOutline(x, y)) { leds[XY(x, y)] = MAGENTA; } if (sandState[XY(x, y)] == 1) { leds[XY(x, y)] = YELLOW; } } } // Draw final 00 drawCountdown(0); FastLED.show(); delay(50); // return; } // Draw background colors first for (uint8_t y = 0; y < GRID_HEIGHT; y++) { for (uint8_t x = 0; x < GRID_WIDTH; x++) { if (isInsideHourglass(x, y) && !isHourglassOutline(x, y)) { leds[XY(x, y)] = PALE_RED; // Inside hourglass background } else if (!isInsideHourglass(x, y)) { leds[XY(x, y)] = PALE_PURPLE; // Outside hourglass background } } } // Draw the hourglass outline for (uint8_t y = 0; y < GRID_HEIGHT; y++) { for (uint8_t x = 0; x < GRID_WIDTH; x++) { if (isHourglassOutline(x, y)) { leds[XY(x, y)] = MAGENTA; } } } // Draw sand particles for (uint8_t y = 0; y < GRID_HEIGHT; y++) { for (uint8_t x = 0; x < GRID_WIDTH; x++) { if (sandState[XY(x, y)] == 1) { leds[XY(x, y)] = YELLOW; } } } // Update and draw falling particle if (!animationComplete) { updateFallingParticle(); // Draw falling particle if (fallingParticle) { leds[XY(fallingParticleX, fallingParticleY)] = YELLOW; } } // Draw countdown numbers drawCountdown(remainingSeconds); FastLED.show(); delay(50); // Slow down animation to 20fps // return; }